<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div id="container">
        {{ msg }}<br>
        <input class="inp-text" type="text" v-model="inpText">
        <p>{{ inpText }}</p>
        <div class="text-box">
            <p class="show-text">{{ msg }}</p>
        </div>
    </div>
    <script>
        //  https://segmentfault.com/a/1190000018659584?ivk_sa=1024320u

        //发布者
        /*comment by liuxf 2021.10.10
        *

            1、构建虚拟DOM，通过compile，建立VM.Data（属性值）与Node的关系，获取VM.Data向Node赋值，触发getter，从而讲订阅者加入发布者。
                node.value = vm[name];
                *
                注意：此处可能会有多个node(element) 对应vm[name]所以会有多个订阅者
                * 如以下代码：有两个{{ msg }}，为两个订阅者。发布者vm['msg]'
                {{ msg }}<br>
                <p class="show-text">{{ msg }}</p>

                 var vm = new Vue({
                    el: 'container',
                    data: {
                        msg: 'Hello world!',
                        inpText: 'Input text'
                    }
                });
                *
                *
            2、修改v-model属性元素 -> 触发修改vm的属性值 -> 触发set->发布者 -> notify分发订阅 -> 订阅者update数据


        */
        function Publisher(){
            this.subs = [];
        }
        Publisher.prototype = {
            constructor: Publisher,
            add: function(sub){//添加订阅者
                this.subs.push(sub);
            },
            notify: function(){//通知订阅者
                this.subs.forEach(function(sub){
                    sub.update();
                });
            }
        };

        //订阅者是节点元素（element），Compile的时候创建订阅者
        // update 采用 node.value=vm[name]进行节点赋值
        // 从而触发vm[name]的getter函数，在getter函数中讲订阅者Subscriber加入了发布者publisher的subs.
        function Subscriber(node, vm, name){
            Publisher.global = this;
            this.node = node;
            this.vm =   vm;
            this.name = name;
            this.update(); //订阅者更新数据
            Publisher.global = null; // 清空
        }
        Subscriber.prototype = {
            constructor: Subscriber,
            update: function(){
                let vm = this.vm;
                let node = this.node;
                let name = this.name;
                switch(this.node.nodeType){
                    case 1:
                        node.value = vm[name];
                        break;
                    case 3:
                        node.nodeValue = vm[name];
                        break;
                    default:
                        break;
                }
            }
        };

        //虚拟节点node 根节点  vm指vue
        function virtualDom(node, vm){
            let frag = document.createDocumentFragment();
            let child;
            // 遍历dom节点
            while(child = node.firstChild){
                compile(child, vm);//通过编译建立发布者 Data与订阅者节点（element）的关系。
                frag.appendChild(child);
            }
            return frag;
        }

        //发布者是vm.Data ，订阅者是节点元素。当数据发生变化时，元素也变化。 by liuxfß
        //创建每个属性的订阅者，并加入到发布者。
        function compile(node, vm){//data vue对象
            let reg = /\{\{(.*)\}\}/g;
            if(node.nodeType === 1){ // 标签
                let attr = node.attributes;
                for(let i = 0, len = attr.length; i < len; i++){
                    console.log(attr[i].nodeName, attr[i].nodeValue);

                    if(attr[i].nodeName === 'v-model'){
                        let name = attr[i].nodeValue;
                        // ------------------------添加监听事件
                        node.addEventListener('keyup', function(e){
                            vm[name] = e.target.value;
                        }, false);
                        new Subscriber(node, vm, name);//制定订阅者，触发getter
                    }
                }
                if(node.hasChildNodes()){
                    node.childNodes.forEach((item) => {
                        compile(item, vm);
                    });
                }
            }
            if(node.nodeType === 3){ // 文本节点
                if(reg.test(node.nodeValue)){
                    let name = RegExp.$1;
                    name = name.trim();
                    new Subscriber(node, vm, name);
                }
            }
        }

        /*

        // 给vue的每个属性增加getter,setter。定义key的属性。
        //创建虚拟DOM时，触发getter，订阅者加入publisher
        //修改数据时，触发setter，触发订阅者。
            node.addEventListener('keyup', function(e){
                vm[name] = e.target.value;  触发setter
            }, false);
        // 参数：  (vm, key, data[key]);  data ： key，value   例如 msg  ： hello world
        */
        function defineReact(vm, key, value){
            let publisher = new Publisher();
            Object.defineProperty(vm, key, {
                set: function(newValue){
                    console.log(`触发setter`);
                    value = newValue;
                    console.log(value);
                    publisher.notify(); // 发布
                },
                get: function(){
                    console.log('订阅者：'+ Publisher.global.name+`触发getter`);
                    if(Publisher.global){
                        //global在订阅者初始化时进行赋值，代表这个订阅者。同时订阅者初始化时触发了getter。
                        publisher.add(Publisher.global); // 添加订阅者
                    }
                    return value;
                }
            });
        }



        //将每个data属性绑定到Vue的实例vm上，
        // 通过defineProperty为vm每个key构建getter，setter属性。
        /*
        例如 vm[msg].getter vm[msg].setter
            vm[inpText].getter vm[inpText].setter

         data: {
            msg: 'Hello world!',
            inpText: 'Input text'
        }
        * */
        function observe(data, vm){
            Object.keys(data).forEach((key) => {
                //data ： key，value  键值对。 例如 msg  ： hello world
                defineReact(vm, key, data[key]);
            })
        }


        //定义VUE
        function Vue(options){

            this.data = options.data;//数据
            let id = options.el;//元素


            observe(this.data, this); //

            //------------------------
            let container = document.getElementById(id);


            let fragment = virtualDom(container, this);
            container.appendChild(fragment);

        }


        var vm = new Vue({
            el: 'container',
            data: {
                msg: 'Hello world!',
                inpText: 'Input text'
            }
        });
        </script>
</body>
</html>
